//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace Microsoft.Research.DataLayer
{
    /// <summary>
    /// Utility class for exporting objects 
    /// </summary>
    public class ExportHelper
    {
        /// <summary>
        /// This method will export the objects in the list to
        /// the store referenced to by the target connection.
        /// For each object that is exported the mandatory arguments are
        /// exported by default.
        /// </summary>
        /// <param name="objList"></param>
        /// <param name="targetConnection"></param>
        public static void ExportObjects(List<IObject> objList, Connection source, Connection targetConnection)
        {
            // Check if the list contains sequence activities. If yes
            // then traverse the activity tree and add all children as objects
            // to be added.
            originialObjList = objList;
            objectsToTransfer = objList;
            sourceConnection = source;
            try
            {
                typeInits = new List<TypeInitializer>(TypeInitializer.CommonSearches.GetAvailableTypeInitializers(sourceConnection));
            }
            catch (RecordNotFoundException)
            {
                typeInits = new List<TypeInitializer>();
            }

            do
            {
                foreach (IObject obj in objectsToTransfer)
                {
                    CopyObject(targetConnection, obj);
                }
                objectsToTransfer.Clear();
                objectsToTransfer.AddRange(newObjList);
                newObjList.Clear();
            } while (objectsToTransfer.Count != 0);

            // Sanitize parameter assignments.
            Dictionary<ParameterAssignment, ParameterAssignment>.Enumerator iter = ExportHelper.paramAssignHash.GetEnumerator();
            while (iter.MoveNext())
            {
                ParameterAssignment oldParam = iter.Current.Key;
                if (oldParam.BoundTo != null)
                {
                    ParameterAssignment newParam = iter.Current.Value;
                    newParam.BoundTo = targetObjects[((IObject)oldParam.BoundTo).ID].ExportedObject as ParameterAssignment;
                }
            }

            targetObjects.Clear();
            sourceTargetMap.Clear();
            paramAssignHash.Clear();
            if (null != objectsToTransfer)
                objectsToTransfer.Clear();
            newObjList.Clear();
            if (null != originialObjList)
                originialObjList.Clear();
        }

        /// <summary>
        /// Depending on the object type call the specific copy method.
        /// </summary>
        /// <param name="obj"></param>
        private static void CopyObject(Connection targetConnection, IObject obj)
        {
            if (obj is Activity)
            {
                CopyActivity(targetConnection, obj as Activity);
            }
            else if (obj is Tag)
            {
                CopyTag(targetConnection, obj as Tag);
            }
            else if (obj is AssemblyPackage)
            {
            }
            else if (obj is AssemblyImage)
            {
            }
            else if (obj is ActivitySequence)
            {
                CopyActivitySeq(targetConnection, obj as ActivitySequence);
            }
            else if (obj is Job)
            {
                CopyJob(targetConnection, obj as Job);
            }
            else if (obj is ActivityInstance)
            {
                CopyActivityInstance(targetConnection, obj as ActivityInstance);
            }
            else if (obj is TypeInitializer)
            {
                CopyTypeInitializer(targetConnection, obj as TypeInitializer);
            }
        }

        private static void CopyJob(Connection targetConnection, Job job)
        {
            bool isNew = false;

            // Copy the user information only if the user information for the job
            // only if the user object is scheduled to be copied as part of the
            // original object list.
            ExportInfo userInfo = null;
            if (ContainsID(((IObject)job.ScheduledBy).ID, ref originialObjList))
            {
                userInfo = CheckAndCopy(targetConnection, job.ScheduledBy, null, out isNew);
            }

            List<object> jobParams = new List<object>();
            if (null != userInfo)
                jobParams.Add(userInfo.ExportedObject);

            ExportInfo jobInfo = CheckAndCopy(targetConnection, job, jobParams, out isNew);
            if (jobInfo.IsInitialized)
                return;
            Job newJob = jobInfo.ExportedObject as Job;

            foreach (ActivityInstance instance in job.ActivityInstances)
            {
                ExportInfo actInfo = CheckAndCopy(targetConnection, instance.Activity, null, out isNew);
                if (!actInfo.IsInitialized &&
                    (false == ContainsID(((IObject)instance.Activity).ID, ref newObjList)))
                    newObjList.Add(instance.Activity);

                jobParams.Clear();
                jobParams.Add(newJob);
                jobParams.Add(actInfo.ExportedObject as Activity);

                ExportInfo actInstanceInfo = CheckAndCopy(targetConnection, instance, jobParams, out isNew);
                if (!actInstanceInfo.IsInitialized &&
                    (false == ContainsID(((IObject)instance).ID, ref newObjList)))
                    newObjList.Add(instance);

                jobParams.Clear();
                jobParams.Add(newJob);
                foreach (ActivityInstanceParameter reqParam in instance.ActivityInstanceParameters)
                {
                    ExportInfo info = CheckAndCopy(targetConnection, reqParam, jobParams, out isNew);
                    targetObjects[((IObject)reqParam).ID].IsInitialized = true;
                }
            }

            foreach (AdditionalParameter customParam in job.AdditionalParameters)
            {
                ExportInfo info = CheckAndCopy(targetConnection, customParam, jobParams, out isNew);
                targetObjects[((IObject)customParam).ID].IsInitialized = true;
            }

            targetObjects[((IObject)job).ID].IsInitialized = true;
        }

        private static void CopyActivityInstance(Connection targetConnection, ActivityInstance instance)
        {
            bool isNew = false;

            // To copy the activity instance make sure that the two required
            // objects, job and activity are created first.
            ExportInfo userInfo = null;
            if (ContainsID(((IObject)instance.Jobs[0].ScheduledBy).ID, ref originialObjList))
            {
                userInfo = CheckAndCopy(targetConnection, instance.Jobs[0].ScheduledBy, null, out isNew);
            }

            List<object> jobParams = new List<object>();
            if (null != userInfo)
                jobParams.Add(userInfo.ExportedObject);
            ExportInfo jobInfo = CheckAndCopy(targetConnection, instance.Jobs[0], jobParams, out isNew);
            if (!jobInfo.IsInitialized &&
                (false == ContainsID(((IObject)instance.Jobs[0]).ID, ref newObjList)))
                newObjList.Add(instance.Jobs[0]);

            ExportInfo actInfo = CheckAndCopy(targetConnection, instance.Activity, null, out isNew);
            if (!actInfo.IsInitialized &&
                (false == ContainsID(((IObject)instance.Activity).ID, ref newObjList)))
                newObjList.Add(instance.Activity);

            List<object> actInsParams = new List<object>();
            actInsParams.Add(jobInfo.ExportedObject);
            actInsParams.Add(actInfo.ExportedObject);
            ExportInfo actInsInfo = CheckAndCopy(targetConnection, instance, actInsParams, out isNew);
            if (actInsInfo.IsInitialized)
                return;

            ActivityInstance targetInstance = actInsInfo.ExportedObject as ActivityInstance;
            foreach (DataProduct product in instance.Outputs)
            {
                List<object> dataParams = new List<object>();
                dataParams.Add(targetInstance);
                ExportInfo dataProdInfo = CheckAndCopy(targetConnection, product, dataParams, out isNew);
                if (dataProdInfo.IsInitialized)
                    continue;
                DataProduct newDp = dataProdInfo.ExportedObject as DataProduct;
                newDp.Contents = product.Contents;
                newDp.Preview = product.Preview;
                targetInstance.Outputs.Add(newDp);
                targetObjects[((IObject)product).ID].IsInitialized = true;
            }
            targetObjects[((IObject)instance).ID].IsInitialized = true;
        }

        private static void CopyTag(Connection targetConnection, Tag tag)
        {
            bool isNew = false;
            ExportInfo tagInfo = CheckAndCopy(targetConnection, tag, null, out isNew);
            targetObjects[((IObject)tag).ID].IsInitialized = true;
        }

        private static void CopyTypeInitializer(Connection targetConnection, TypeInitializer typeInitializer)
        {
            bool isNew = false;

            List<object> copyArgs = new List<object>() { typeInitializer.RelatedType };
            ExportInfo typeInitInfo = CheckAndCopy(targetConnection, typeInitializer, copyArgs, out isNew);
            if (typeInitInfo.IsInitialized)
                return;

            targetObjects[((IObject)typeInitializer).ID].IsInitialized = true;

            TypeInitializer newTypeInit = typeInitInfo.ExportedObject as TypeInitializer;

            // Copy all the binaries for the WF. For each package add
            // images and for image copy the contents explicitly.
            foreach (AssemblyPackage package in typeInitializer.AssemblyPackages)
            {
                ExportInfo packageInfo = CheckAndCopy(targetConnection, package, null, out isNew);
                AssemblyPackage newPackage = packageInfo.ExportedObject as AssemblyPackage;
                newTypeInit.AssemblyPackages.Add(newPackage);

                if (packageInfo.IsInitialized)
                    continue;
                foreach (AssemblyImage image in package.AssemblyImages)
                {
                    ExportInfo assemblyInfo = CheckAndCopy(targetConnection, image, null, out isNew);
                    if (assemblyInfo.IsInitialized == true)
                        continue;
                    AssemblyImage targetImage = assemblyInfo.ExportedObject as AssemblyImage;
                    newPackage.AssemblyImages.Add(targetImage);
                    if (!assemblyInfo.IsInitialized)
                        targetImage.Contents = image.Contents;
                    targetObjects[((IObject)image).ID].IsInitialized = true;
                }
                targetObjects[((IObject)package).ID].IsInitialized = true;
            }
        }

        private static void CopyActivitySeq(Connection targetConnection, ActivitySequence seq)
        {
            bool isNew = false;

            // To copy the activity sequence it is necessary to have the 
            // activity object. This first create the activity object in 
            // target or get a reference if the object alreay exists.
            ExportInfo activityInfo = CheckAndCopy(targetConnection, seq.Activity, null, out isNew);
            if (!activityInfo.IsInitialized &&
                (false == ContainsID(((IObject)seq.Activity).ID, ref newObjList)))
                newObjList.Add(seq.Activity);

            List<object> copyArgs = new List<object>();
            copyArgs.Add(activityInfo.ExportedObject);
            ExportInfo seqInfo = CheckAndCopy(targetConnection, seq, copyArgs, out isNew);

            // If this sequence is already initialized then return.
            if (seqInfo.IsInitialized)
                return;

            ActivitySequence newSeq = seqInfo.ExportedObject as ActivitySequence;

            // Copy over all the child activity sequences. If they are not part of the
            // exported list then add them to the list of objects to be exported.
            foreach (ActivitySequence childSeq in seq.ActivitySequences)
            {
                activityInfo = CheckAndCopy(targetConnection, childSeq.Activity, null, out isNew);
                if (!activityInfo.IsInitialized &&
                (false == ContainsID(((IObject)childSeq.Activity).ID, ref newObjList)))
                    newObjList.Add(childSeq.Activity);

                copyArgs.Clear();
                copyArgs.Add(activityInfo.ExportedObject);

                ExportInfo childSeqInfo = CheckAndCopy(targetConnection, childSeq, copyArgs, out isNew);
                ((ActivitySequence)childSeqInfo.ExportedObject).Parent = newSeq;

                if (!childSeqInfo.IsInitialized &&
                    (false == ContainsID(((IObject)childSeq).ID, ref newObjList)))
                    newObjList.Add(childSeq);
            }

            foreach (ParameterAssignment paramAssign in seq.ParameterAssignments)
            {
                List<Object> arguments = new List<object>() { newSeq };
                ExportInfo paramInfo = CheckAndCopy(targetConnection, paramAssign, arguments, out isNew);
                ParameterAssignment newParamAssign = paramInfo.ExportedObject as ParameterAssignment;
                targetObjects[((IObject)paramAssign).ID].IsInitialized = true;

                ExportHelper.paramAssignHash.Add(paramAssign, newParamAssign);
            }

            targetObjects[((IObject)seq).ID].IsInitialized = true;
        }

        /// <summary>
        /// Copy the activity object and its dependencies which include
        /// a. Package
        /// b. Assemblies
        /// c. Activity Sequences
        /// </summary>
        /// <param name="activity"></param>
        private static void CopyActivity(Connection targetConnection, Activity activity)
        {
            bool isNew = false;
            ExportInfo info = CheckAndCopy(targetConnection, activity, null, out isNew);
            Activity workflow = info.ExportedObject as Activity;
            if (info.IsInitialized)
                return;

            // Copy all the binaries for the WF. For each package add
            // images and for image copy the contents explicitly.
            foreach (AssemblyPackage package in activity.AssemblyPackages)
            {
                ExportInfo packageInfo = CheckAndCopy(targetConnection, package, null, out isNew);
                AssemblyPackage newPackage = packageInfo.ExportedObject as AssemblyPackage;
                workflow.AssemblyPackages.Add(newPackage);

                if (packageInfo.IsInitialized)
                    continue;
                foreach (AssemblyImage image in package.AssemblyImages)
                {
                    ExportInfo assemblyInfo = CheckAndCopy(targetConnection, image, null, out isNew);
                    if (assemblyInfo.IsInitialized == true)
                        continue;
                    AssemblyImage targetImage = assemblyInfo.ExportedObject as AssemblyImage;
                    newPackage.AssemblyImages.Add(targetImage);
                    if (!assemblyInfo.IsInitialized)
                        targetImage.Contents = image.Contents;
                    targetObjects[((IObject)image).ID].IsInitialized = true;
                }
                targetObjects[((IObject)package).ID].IsInitialized = true;
            }

            foreach (ActivityParameter param in activity.ActivityParameters)
            {
                TypeInitializer typeInit = typeInits.FirstOrDefault(T => T.RelatedType.TypeName.Equals(param.Type, StringComparison.Ordinal));
                if (typeInit != null)
                {
                    newObjList.Add(typeInit);
                }

                ExportInfo paramInfo = CheckAndCopy(targetConnection, param, null, out isNew);
                targetObjects[((IObject)param).ID].IsInitialized = true;
            }

            foreach (Tag tag in activity.Tags)
            {
                if (ContainsID(((IObject)tag).ID, ref originialObjList))
                {
                    ExportInfo tagInfo = CheckAndCopy(targetConnection, tag, null, out isNew);
                    workflow.Tags.Add(tagInfo.ExportedObject as Tag);
                    targetObjects[((IObject)tag).ID].IsInitialized = true;
                }
            }
            targetObjects[((IObject)activity).ID].IsInitialized = true;
        }

        private static ExportInfo CheckAndCopy(Connection targetConn, IObject sourceObject,
            List<object> copyArgs, out bool isNew)
        {
            if (sourceTargetMap.ContainsKey(sourceObject.ID))
            {
                isNew = false;
                return targetObjects[sourceObject.ID];
            }

            ExportInfo info = new ExportInfo();
            info.ExportedObject = CopyObjectInternal(targetConn, sourceObject, copyArgs);
            info.IsInitialized = false;
            isNew = true;
            sourceTargetMap.Add(sourceObject.ID, info.ExportedObject.ID);
            targetObjects.Add(sourceObject.ID, info);
            return info;
        }

        /// <summary>
        /// This method calls the copy method for the source object. Some 
        /// objects require speical code for copying like references to
        /// other target objects as parameters. In these cases the copy helpers
        /// are used. The additional arguments that are used are passed as 
        /// objects in a list.
        /// </summary>
        /// <param name="targetConn"></param>
        /// <param name="sourceObj"></param>
        /// <param name="copyArgs"></param>
        /// <returns></returns>
        private static IObject CopyObjectInternal(Connection targetConn, IObject sourceObj, List<object> copyArgs)
        {
            if (sourceObj is ActivitySequence)
            {
                if (!(copyArgs[0] is Activity))
                    throw new ArgumentException("Necessary arguments for creating activity sequence missing.");
                ActivitySequence.CopyHelper helper = new ActivitySequence.CopyHelper(null, copyArgs[0] as Activity);
                return ((ActivitySequence)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is Job)
            {
                Job.CopyHelper helper = null;
                if (copyArgs.Count > 1)
                    helper = new Job.CopyHelper(copyArgs[0] as User);
                else
                    helper = new Job.CopyHelper(null);
                return ((Job)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is ActivityInstance)
            {
                if ((copyArgs.Count != 2) || (!(copyArgs[0] is Job)) || (!(copyArgs[1] is Activity)))
                    throw new ArgumentException("Necessary arguments for creating activity instance missing.");

                ActivityInstance.CopyHelper helper = new ActivityInstance.CopyHelper(copyArgs[1] as Activity);
                return ((ActivityInstance)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is DataProduct)
            {
                if (!(copyArgs[0] is ActivityInstance))
                    throw new ArgumentException("Necessary arguments for creating activity instance missing.");

                DataProduct.CopyHelper helper =
                    new DataProduct.CopyHelper(copyArgs[0] as ActivityInstance, null, null);
                return ((DataProduct)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is User)
            {
                User.CopyHelper helper = new User.CopyHelper(null, null, null);
                return ((User)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is ActivityParameter)
            {
                ActivityParameter actParam = sourceObj as ActivityParameter;
                Activity parentActivity = targetObjects[((IObject)actParam.Activity).ID].ExportedObject as Activity;
                ActivityParameter.CopyHelper helper = new ActivityParameter.CopyHelper(parentActivity);
                return actParam.CopyTo(targetConn, helper);
            }
            else if (sourceObj is ParameterAssignment)
            {
                ParameterAssignment paramAssign = sourceObj as ParameterAssignment;
                ActivityParameter actParam = targetObjects[((IObject)paramAssign.ActivityParameter).ID].ExportedObject as ActivityParameter;
                ParameterAssignment.CopyHelper helper =
                    new ParameterAssignment.CopyHelper(actParam, copyArgs[0] as ActivitySequence, paramAssign.BoundTo);
                return paramAssign.CopyTo(targetConn, helper);
            }
            else if (sourceObj is TypeInitializer)
            {
                TypeInitializer typeInit = sourceObj as TypeInitializer;
                TypeInitializer.CopyHelper helper = new TypeInitializer.CopyHelper(copyArgs[0] as TridentTypeInfo);
                return typeInit.CopyTo(targetConn, helper);
            }
            else if (sourceObj is Tag)
            {
                return ((Tag)sourceObj).CopyTo(targetConn);
            }
            else if (sourceObj is AssemblyImage)
            {
                AssemblyImage.CopyHelper helper = new AssemblyImage.CopyHelper(null);
                return ((AssemblyImage)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is AssemblyPackage)
            {
                return ((AssemblyPackage)sourceObj).CopyTo(targetConn);
            }
            else if (sourceObj is Activity)
            {
                Activity.CopyHelper helper = new Activity.CopyHelper(null, null, null);
                return ((Activity)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is AdditionalParameter)
            {
                if ((copyArgs.Count != 1) || (
                    !(copyArgs[0] is Job)))
                    throw new ArgumentException("Necessary arguments for creating custom parameter missing.");
                AdditionalParameter.CopyHelper helper = new AdditionalParameter.CopyHelper(copyArgs[0] as Job);
                return ((AdditionalParameter)sourceObj).CopyTo(targetConn, helper);
            }
            else if (sourceObj is ActivityInstanceParameter)
            {
                if ((copyArgs.Count != 2) || (
                    !(copyArgs[0] is Job)))
                    throw new ArgumentException("Necessary arguments for creating required parameter missing.");
                ActivityInstanceParameter.CopyHelper helper =
                    new ActivityInstanceParameter.CopyHelper(copyArgs[0] as ActivityInstance, copyArgs[1] as ParameterAssignment);
                return ((ActivityInstanceParameter)sourceObj).CopyTo(targetConn, helper);
            }
            else
            {
                throw new InvalidOperationException("Code should never reach here.");
            }
        }

        /// <summary>
        /// Check if the object exists in the list of objects to be
        /// exported.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="objList"></param>
        /// <returns></returns>
        private static bool ContainsID(Guid id, ref List<IObject> objList)
        {
            foreach (IObject obj in objList)
            {
                if (id == obj.ID)
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Map of object ids from the source registry to objects in
        /// the target registry.
        /// </summary>
        private static Dictionary<Guid, ExportInfo> targetObjects = new Dictionary<Guid, ExportInfo>();

        /// <summary>
        /// Map of ids from the source registry to corresponding objects
        /// in the target registry.
        /// </summary>
        private static Dictionary<Guid, Guid> sourceTargetMap = new Dictionary<Guid, Guid>();

        /// <summary>
        /// Map of existing and new parameter assignments.
        /// </summary>
        private static Dictionary<ParameterAssignment, ParameterAssignment> paramAssignHash =
            new Dictionary<ParameterAssignment, ParameterAssignment>();

        /// <summary>
        /// Input list of objects to be transferred.
        /// </summary>
        private static List<IObject> objectsToTransfer = null;

        private static List<IObject> newObjList = new List<IObject>();

        private static List<IObject> originialObjList = null;

        private static Connection sourceConnection;

        private static List<TypeInitializer> typeInits;
    }

    /// <summary>
    /// Data class for storing information about the exported
    /// objects and their state. The boolean IsInitalized indicates
    /// if the object has been completely exported. A complete
    /// export means with all the mandatory relationships.
    /// </summary>
    internal class ExportInfo
    {
        public IObject ExportedObject
        {
            get { return this.exportedObject; }
            set { this.exportedObject = value; }
        }

        public bool IsInitialized
        {
            get { return this.isInitialized; }
            set { this.isInitialized = value; }
        }

        private IObject exportedObject;

        private bool isInitialized;
    }
}
